package com.practice.chatroom;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.Socket;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

/**
 * 本类专注于解析request及写response
 */
public class Deal extends Thread {
    private ChatRoomServer svr;
    private Socket socket;
    private InputStream is;
    private OutputStream os;
    /** 请求所带cookie */
    private Map<String, String> reqCookie = new HashMap<>();
    /** 我方准备在响应中设的cookie */
    private Map<String, String> respSetCookie = new HashMap<>();
    /** 本次接受到的请求来自浏览器时，其头部 */
    private List<String> header = new ArrayList<>();
    /** 本次接受到的请求来自浏览器时，其方法类型 */
    private String reqMethodType;

    /**
     * 初始化交互线程实例
     * 
     * @param svr    服务端
     * @param socket 本次交互socket
     */
    public Deal(ChatRoomServer svr, Socket socket) {
        this.svr = svr;
        this.socket = socket;
    }

    /** 开始处理请求 */
    @Override
    public void run() {
        try {
            is = socket.getInputStream();
            List<Integer> line = new ArrayList<>();
            while (true) { // 先一股脑读入全部请求头
                int i = is.read();
                if (i == 13) {
                    if (line.isEmpty()) {
                        break; // 首个空行后的换行视为请求头读完，openjfx的gui client也遵从
                    }
                    header.add(new String(
                            ArrayUtils.toPrimitive(line.stream().map(Integer::byteValue).toArray(Byte[]::new)),
                            "UTF-8")); // 考虑到各种情况，读完一整行再整体以utf8转为字符串，才保险
                    line.clear();
                } else if (i == 10) { // 标准的换行为连续的\r\n
                } else {
                    line.add(i);
                }
            }
            if (header.size() < 1) {
                return;
            }
            os = socket.getOutputStream();
            String head0 = header.get(0);
            if (head0.toUpperCase().indexOf("HTTP") > -1) { // 简单判定是否为来自浏览器的请求
                String ContentType = null;
                String contentLength = null;
                boolean isWS = false;
                try {
                    String wsKey = null;
                    for (String headLine : header) { // 遍历请求头每一行，收集可能用到的信息
                        if (headLine.startsWith("Cookie:")) {
                            reqCookie = svr.analysisStr(URLDecoder.decode(headLine.substring(7), "UTF-8"), ";", "="); // 为支持中文用户名，cookie需解码
                        }
                        if (headLine.startsWith("Content-Length:")) {
                            contentLength = headLine.substring(headLine.indexOf(":") + 1).trim();
                        }
                        if (headLine.startsWith("Content-Type:")) {
                            ContentType = headLine.substring(headLine.indexOf(":") + 1).trim();
                        }
                        if (headLine.equals("Upgrade: websocket")) { // 以下两项都是ws特有的头部
                            isWS = true;
                        }
                        if (headLine.startsWith("Sec-WebSocket-Key:")) {
                            wsKey = headLine.substring(headLine.indexOf(":") + 1).trim();
                        }
                    }
                    String[] firstLine = head0.split(" ");
                    reqMethodType = firstLine[0];
                    String reqUrl = firstLine[1];
                    svr.logger.info(String.format("本次请求在第一个完整空行前的全部信息为：%n%s%n",
                            header.stream().collect(Collectors.joining("\r\n"))));
                    if (isWS) { // 特殊的websocket单独处理
                        svr.buildWS(reqCookie.get("sessionId"), socket, wsKey);
                    } else { // 传统请求统一入口
                        acReq(reqUrl, ContentType, contentLength);
                    }
                } catch (AppException e) { // 处理中任意校验不过，都应throw这个自定义异常，由此统一返回报错页面
                    e.printStackTrace();
                    respError(e);
                }
            } else if (head0.equals("CLIENT")) { // 来自openjfx的gui client
                new ChatRoomBus(svr, socket, header);
            } else {
                svr.logger.warning(String.format("接收到未知请求：%n%s%n", header.toString()));
            }
            // 本if后线程应结束，不能再有任何处理
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 处理传统http请求
     * 
     * @param reqUrl        请求地址
     * @param contentType   实体类型
     * @param contentLength 实体长度
     * @throws AppException 可能会抛给前台的错误提示
     */
    private void acReq(String reqUrl, String contentType, String contentLength) throws AppException {
        try {
            Map<String, String> prmMap = new HashMap<>();
            switch (reqMethodType.toUpperCase()) {
            case "POST": // 目前暂定有参数都必须走post
                if (contentLength.isEmpty()) {
                    throw new AppException("POST请求必须有Content-Length！", HttpStatusCode.LENGTH_REQUIRED_411);
                }
                int len = Integer.parseInt(contentLength);
                is.read(); // 空行后再另起一行开始是参数
                String param = "";
                for (int i = 0; i < len; i++) {
                    param += (char) is.read();
                }
                prmMap = svr.analysisStr(URLDecoder.decode(param, "UTF-8"), "&", "="); // 为支持中文消息，传参需解码
                svr.logger.info(String.format("本次POST请求的参数串为：%s%n", prmMap));
            case "GET": // 上个case做额外的参数收集，然后在此统一做后续处理
                Resp response = handleURL(reqUrl, prmMap);
                if (reqUrl.equals("/webapp/pages/chatRoomLogin.html")) {
                    BigInteger[] RSApublicKey = svr.getNewRSA();
                    respSetCookie.put("module", String.valueOf(RSApublicKey[0]));
                    respSetCookie.put("pubKey", String.valueOf(RSApublicKey[1]));
                }
                respSetCookie.putAll(response.getCookie()); // 可能要set额外的cookie
                doResp(response);
                break;
            default:
                throw new AppException("不支持的HTTP请求类型！", HttpStatusCode.METHOD_NOT_ALLOWED_405);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 所有的中间处理统一放在本方法，任意时机结束仅需各自return，确保统一回到 acReq 的响应出口
     * 
     * @param reqUrl 请求地址
     * @param param  请求参数
     * @return 响应实例
     * @throws AppException 可能会抛给前台的错误提示
     * @see com.practice.chatroom.Resp
     */
    private Resp handleURL(String reqUrl, Map<String, String> param) throws AppException {
        String sessionId = reqCookie.getOrDefault("sessionId", null);
        String newSessionId = svr.validateSession(sessionId, reqCookie.get("username"));
        if (!StringUtils.isEmpty(newSessionId)) { // 不为空即验证通过
            if (!newSessionId.equals(sessionId)) { // 若返回的session变了说明到期更换了新的
                respSetCookie.put("sessionId", newSessionId);
            }
            if (svr.conf.getJSONArray("autoLogUrls").toList().contains(reqUrl.replace("webapp/pages/", ""))) { // 对于所有自动登录跳转的地址，若请求包含有效session则重定向至聊天室主页
                svr.logger.info(String.format("本次对地址 %s 的请求，因包含有效的会话信息，将自动登录并重定向到聊天室页面%n", reqUrl));
                return new Resp(HttpStatusCode.LOCATION_302, "/webapp/pages/chatRoom.html");
            }
        } else { // 对于所有需验证的地址，若请求未包含有效session则重定向至登录页面
            if (svr.conf.getJSONArray("auchUrls").toList().contains(reqUrl.replace("webapp/pages/", ""))) {
                svr.logger.info(String.format("本次对地址 %s 的请求，因未含有效的会话信息，将自动重定向到登录页面%n", reqUrl));
                return new Resp(HttpStatusCode.LOCATION_302, "/webapp/pages/chatRoomLogin.html");
            }
        }
        if (reqUrl.indexOf(".") > -1) { // 简单判定为文件请求
            return new Resp(HttpStatusCode.OK_200, reqUrl);
        }
        String prmUsername = param.get("username");
        String prmPW = param.get("password");
        String cookieModule = reqCookie.get("module");
        ChatRoomBus business = new ChatRoomBus(svr);
        switch (reqUrl) { // 识别url，调用对应的业务逻辑
        case "/reg": // 注册
            return business.reg(prmUsername, prmPW, param.get("icon"), cookieModule);
        case "/login": // 登录
            return business.login(prmUsername, prmPW, cookieModule);
        case "/sendMsg": // 发消息
            return business.sendMsg(reqCookie.get("username"), Boolean.parseBoolean(param.get("isImg")),
                    param.get("msg"), param.get("color"), param.get("fontSize"));
        case "/exit": // 登出
            return business.exit(sessionId);
        default: // 既不是文件又不是有意义的次级域，则统一重定向至登录页
            return new Resp(HttpStatusCode.LOCATION_302, "webapp/pages/chatRoomLogin.html");
        }
    }

    /**
     * 写响应
     * 
     * @param response 对请求的各种处理后所产生的响应实例
     * @throws AppException 可能会抛给前台的错误提示
     * @throws IOException  socket或文件等io错误
     * @see com.practice.chatroom.Resp
     */
    private void doResp(Resp response) throws AppException, IOException {
        String url = response.getURL();
        String respHead = "";
        HttpStatusCode respType = response.getRespType();
        if (respType.equals(HttpStatusCode.OK_200)) { // 普通响应，可能处理文件，单独写
            File file = new File("./src/main/" + url);
            if (!file.exists()) {
                throw new AppException("请求的资源不存在！", HttpStatusCode.NOT_FOUND_404);
            }
            String suffix = url.substring(url.lastIndexOf(".") + 1);
            if (!CONTENT_TYPE.containsKey(suffix)) {
                throw new AppException("请求的资源类型尚未支持！", HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415);
            }
            if (url.equals("/webapp/pages/chatRoomLogin.html") && !svr.conf.getBoolean("withDB")) {
                respSetCookie.put("noDB", "true");
            }
            try (FileInputStream fis = new FileInputStream(file)) {
                respHead = "HTTP/1.1 200 OK\r\nContent-Type:" + CONTENT_TYPE.get(suffix)
                        + (isStream(suffix) ? "" : ";charset=UTF-8") + "\r\n" + getStr4SetCookie() + "\r\n";
                os.write(respHead.getBytes());
                IOUtils.copy(fis, os);
            }
        } else { // 肯定不处理文件的响应
            respHead = "HTTP/1.1 " + respType.toString() + "\r\n";
            switch (respType) {
            case LOCATION_302: // 302重定向
                respHead += "Location:" + url + "\r\n";
                break;
            case NO_CONTENT_204: // 204无内容
                break;
            default:
                throw new AppException("响应类型错误！");
            }
            respHead += getStr4SetCookie();
            os.write(respHead.getBytes());
        }
        svr.logger.info(String.format("进行了响应，响应头信息：%n%s%n", respHead));
        os.flush();
        socket.close();
    }

    /**
     * 向前端显示错误信息或直接响应错误页面
     * 
     * @param err 捕获的业务错误
     * @see com.practice.chatroom.AppException
     */
    private void respError(AppException err) {
        try {
            if (!reqMethodType.equals("POST")) { // 非POST视为ajax以外的情况，或者说不会有失败回调等后续代码，此时返回一个固定页面显示错误信息
                FileInputStream fis = new FileInputStream(new File("./src/main/webapp/index.html"));
                os.write(("HTTP/1.1 " + err.errorCode.toString() + "\r\nContent-Type:text/html;charset=UTF-8\r\n")
                        .getBytes());
                String html = new String(fis.readAllBytes());
                os.write(html.replace("errMsg", err.getMessage()).getBytes()); // 设置错误信息
                os.flush();
                fis.close();
            } else { // POST视为ajax异步请求，此时仅返回错误信息，前台应自行处置
                os.write(("HTTP/1.1 " + err.errorCode.toString() + "\r\nContent-Type:text/plain;charset=UTF-8\r\n\r\n"
                        + err.getMessage() + "\r\n").getBytes("UTF-8"));
                os.flush();
                os.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成response中Set-Cookie的串，来自本次请求线程实例中的全局map变量，其键值对应当在处理过程中先put好
     * 
     * @return 响应头的多行cookie串
     */
    private String getStr4SetCookie() {
        int afkLimit = svr.conf.getInt("afkLimit");
        int sessionAge = svr.conf.getInt("sessionAge");
        int RSAage = svr.conf.getInt("RSAage");
        if (respSetCookie.size() > 0) {
            Map<String, String> map = new HashMap<>(Map.copyOf(reqCookie));
            map.putAll(respSetCookie); // 如果要设置cookie，需在原有的基础上覆盖要新set的
            return map.entrySet().stream().flatMap(entry -> {
                try { // cookie不支持中文，只能转码
                    String cookieKey = entry.getKey();
                    String cookeStr = "Set-Cookie:" + cookieKey + "=" + URLEncoder.encode(entry.getValue(), "UTF-8")
                            + ";Domain=" + svr.getHost() + ";Path=/;";
                    switch (cookieKey) {
                    case "module":
                    case "pubKey":
                        cookeStr += "Max-Age=" + RSAage;
                        break;
                    case "sessionId":
                        cookeStr += "HttpOnly;"; // username允许js获取是因为查聊天记录是浏览器端的本地数据库行为，要辨识自身消息区别昵称字色，不像别处都由服务器判定返回
                    default:
                        cookeStr += "Max-Age=" + afkLimit + sessionAge; // cookie保留session的时长，应为暂离限制与session更换时间之和
                    }
                    return Stream.of(cookeStr);
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                    return null;
                }
            }).collect(Collectors.joining("\r\n")) + "\r\n";
        } else {
            return "";
        }
    }

    /** HTTP状态码 */
    public static enum HttpStatusCode {
        OK_200("200 OK"), NO_CONTENT_204("204 No Content"), LOCATION_302("302 Move temporarily"),
        BAD_REQUEST_400("400 Bad Request"), NOT_FOUND_404("404 Not Found"),
        METHOD_NOT_ALLOWED_405("405 Method Not Allowed"), LENGTH_REQUIRED_411("411 Length Required"),
        UNSUPPORTED_MEDIA_TYPE_415("415 Unsupported Media Type"), TOO_MANY_CONNECTIONS_421("421 too many connections");

        private final String head;

        private HttpStatusCode(String head) {
            this.head = head;
        }

        @Override
        public String toString() {
            return head;
        }
    }

    /**
     * 判断是否为需要读文件的请求
     * 
     * @param suffix 本次请求url的后缀
     * @return 是否为文件
     */
    private boolean isStream(String suffix) {
        return !List.of("html", "css", "js").contains(suffix);
    }

    /** 文件后缀名与其MIME type值的对照 */
    private static final Map<String, String> CONTENT_TYPE = new HashMap<>();
    static {
        CONTENT_TYPE.put("html", "text/html");
        CONTENT_TYPE.put("css", "text/css");
        CONTENT_TYPE.put("js", "application/x-javascript");
        CONTENT_TYPE.put("jpg", "image/jpeg");
        CONTENT_TYPE.put("png", "image/png");
        CONTENT_TYPE.put("gif", "image/gif");
    }

}